Skip to content

Speed up mount path: cheap arity check, reuse first(), leaner emit - #186

Merged
berendkleinhaneveld merged 4 commits into
masterfrom
perf/mount-path
Jul 7, 2026
Merged

Speed up mount path: cheap arity check, reuse first(), leaner emit#186
berendkleinhaneveld merged 4 commits into
masterfrom
perf/mount-path

Conversation

@berendkleinhaneveld

Copy link
Copy Markdown
Collaborator

Summary

Second PR of the performance series (stacked on #185; will retarget to master automatically when that merges).

  • weak() no longer builds a full inspect.Signature per wrapped watcher callback just to count arguments — it reads __code__.co_argcount instead. This runs for every dynamic bind during create/mount, so it dominates mount cost for elements with bindings.
  • ComponentFragment.mount() resolves the component root element with the existing first() helper instead of a hand-rolled BFS (which also imported deque inside the method on every mount). first() returns the first element in document order, which is also more correct than BFS for uneven subtree depths. Regression test added for a component whose root element sits behind a v-if wrapper.
  • Component.emit() avoids creating a defaultdict entry for events without handlers and skips the set copy when there is nothing to call.

Benchmarks (local, macOS, vs base branch)

benchmark before (mean) after (mean) delta
mount_bound_elements[10] 177.3 µs 124.3 µs -30%
mount_bound_elements[100] 1.66 ms 1.21 ms -27%
mount_bound_elements[1k] 18.2 ms 13.9 ms -24%
mount_nested_components[10/50/200] 271 µs / 1.66 ms / 6.87 ms 261 µs / 1.52 ms / 6.66 ms -3–8%
mount_plain_elements unchanged unchanged ~0%

The benchmark CI workflow will reproduce this comparison automatically once #185 lands.

Test plan

  • uv run pytest tests -q — 350 passed, 1 skipped (new regression test included)
  • uv run ruff check . / uv run ruff format --check . — clean
  • uv run pytest bench --benchmark-only --benchmark-compare — no regressions; gains listed above

🤖 Generated with Claude Code

Adds a bench/ suite (element creation, component mount depth,
attribute update at depth, keyed/unkeyed v-for reconciliation, and
unkeyed list grow/shrink) using pytest-benchmark with DictRenderer and
synchronous event loop, mirroring the setup in the sibling observ repo.

The new benchmark workflow runs the suite twice on every PR - once
against the master version of collagraph/ and once against the PR
version - and fails when mean time regresses more than 5%. The CI test
job is scoped to the tests directory so the matrix does not execute
benchmarks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
berendkleinhaneveld and others added 2 commits July 7, 2026 00:29
The allocation burst of list reconciliation triggers collection pauses
in some rounds, which inflated stddev on the grow benchmarks to ~40%
of the mean - far too noisy for the 5% regression gate in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- weak(): read __code__.co_argcount instead of building a full
  inspect.Signature for every wrapped watcher callback; this runs for
  every dynamic bind during create/mount.
- ComponentFragment.mount(): use the existing first() helper to resolve
  the component root element instead of a hand-rolled BFS with a
  per-mount deque import. Adds a regression test for a component whose
  root element sits behind a v-if wrapper.
- Component.emit(): avoid creating defaultdict entries for events that
  have no handlers and skip the set copy when empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from bench/suite-and-ci to master July 7, 2026 18:09
@berendkleinhaneveld
berendkleinhaneveld merged commit 748d9b9 into master Jul 7, 2026
9 of 10 checks passed
@berendkleinhaneveld
berendkleinhaneveld deleted the perf/mount-path branch July 7, 2026 18:16
berendkleinhaneveld added a commit that referenced this pull request Jul 7, 2026
Resolves conflicts in fragment.py by porting the perf work from
master (PRs #186-#188) onto the two-tree fragment model:

- Keep both _render_parent (branch) and _component_parent_cache
  (master) on Fragment.
- anchor() keeps the unified render_children() traversal, which
  subsumes master's separate children/slot_contents lookups.
- ListFragment.mount() keeps the lazy loop-invariant anchor from
  master, applied to _generated_fragments.
- ComponentFragment.mount() uses master's simpler
  `self.component._element = self.first()`.
- _invalidate_component_parent_cache() traverses via
  iter_all_children() instead of the removed children/slot_contents
  attributes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
berendkleinhaneveld added a commit that referenced this pull request Jul 21, 2026
Features:
- Add pure-Python view API as alternative to cgx templates (#193)
- Support text elements for PySide widgets that display text (#191)

Fixes & internals:
- Fragment parenting overhaul (#162)
- Fix PyInstaller hook for CGX files inside packages (#184)
- Write compiled AST to temp file when CGX_DEBUG is set (#175)

Performance:
- Speed up mount path: cheap arity check, reuse first(), leaner emit (#186)
- Cache Fragment._component_parent() lookups (#187)
- Avoid redundant anchor lookups in Fragment.anchor() and unkeyed v-for (#188)

Documentation:
- Add MkDocs documentation with GitHub Pages deployment (#176)
- Add internals architecture documentation page (#194)
- Add docs badge and links to README (#192)

Tooling & CI:
- Add benchmark suite and per-PR benchmark CI workflow (#185)
- Make benchmark CI guard robust against run-to-run noise (#196)
- Update GitHub actions from Node 20 to Node 24 (#190)
- Migrate from pre-commit to prek (#195)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant